<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" href="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/bootstrap/4.6.1/css/bootstrap.css" />
    <script type="text/javascript" src="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/jquery/3.6.0/jquery.min.js"></script>
    <script type="text/javascript" src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/echarts/5.1.1/echarts.common.min.js"></script>
    <script type="text/javascript" src="https://lf3-cdn-tos.bytecdntp.com/cdn/expire-1-M/bootstrap/5.1.3/js/bootstrap.bundle.min.js"></script>

    <style>
        .flexable, .auto-grow {display: flex}
        .auto-grow {flex-grow: 1}
        .parameter-box {
            display: grid; grid-template-columns: repeat(4, 60px 240px); text-align: right; align-items: center;
            gap: 10px;
        }
        .parameter-box .control-label {margin:0;}
        #log-panel {border-left: 1px solid #ccc; overflow-x: hidden; overflow-y: auto; padding: 10px;}
    </style>
</head>
<body>
<div class="card">
    <div class="card-header flexable" style="justify-content: space-between;align-items: center;">
        Parameters
        <a class="btn btn-primary" id="btn-apply">Apply</a>
    </div>
    <div class="card-body flexable">
        <div class="parameter-box">
            <label class="control-label" for="text-Kp">Kp</label>
            <input id="text-Kp" value="0.75" class="form-control"/>

            <label class="control-label" for="text-Ki">Ki</label>
            <input id="text-Ki" value="0.1" class="form-control"/>

            <label class="control-label" for="text-Kd">Kd</label>
            <input id="text-Kd" value="0.1" class="form-control"/>

            <label class="control-label" for="text-leak">Leak</label>
            <input id="text-leak" value="-5" class="form-control" />

            <label class="control-label" for="text-target">Target</label>
            <input id="text-target" value="533" class="form-control"/>

            <label class="control-label" for="text-value">Initial</label>
            <input id="text-value" value="411" class="form-control"/>

            <label class="control-label" for="text-delta">ε</label>
            <input id="text-delta" value="0.0001" class="form-control"/>
        </div>
    </div>
</div>
<div class="flexable">
    <div id="canvas" class="auto-grow"></div>
    <div id="log-panel">
        <div id="logger" style="width: 600px; font-family: 'Source Code Pro', monospace; font-size: smaller"></div>
    </div>
</div>
<script>
$(function () {
    let p = {
        Kp: 0.75, Ki: 0.1, Kd: 0.1, leak: -5,
        target: 533, /*value: 411, */delta: 0.0001
    }, /*error = 0, sum = 0, last_error,*/ count = 0, data = [[], []],/* matches = 0,*/
    s1 = {error: 0, sum: 0, last_error: 0, value: 280, matches: 0},
    s2 = {error: 0, sum: 0, last_error: 0, value: 390, matches: 0},
    chart, options = {
        tooltip: {
            trigger: 'axis'
        },
        xAxis: {type: 'category'},
        yAxis: {
            type: 'value',
            max: value => {
                let range = value.max - value.min,
                    n = value.max + range / 10;
                return Math.round (n);
            },
            min: value => {
                let range = value.max - value.min,
                    n = value.min - range / 10;
                return Math.round (n);
            }
        },
        series: []
    }, controls = $('.form-control');

    function pid (s) {
        s.error = p.target - s.value;
        s.sum += s.error;
        let n = p.Kp * s.error + p.Ki * s.sum + p.leak;
        if (count > 0) {
            n += p.Kd * (s.error - s.last_error);
        }
        // p.value += n;
        s.last_error = s.error;

        if (Math.abs (p.target - s.value) < p.delta) {
            s.matches ++;
        } else {
            s.matches = 0;
        }
        // return matches >= 5 || count > 1000;
        return n;
    }

    let logger = (function () {
        let root = $('#logger'), colors = ['#92cc79', '#f9c761'], index = 0, replace = false;
        return {
            info : function (msg) {
                if (typeof msg === "object") {
                    msg = JSON.stringify (msg);
                }
                if (arguments.length > 1) for (let i = 1; i < arguments.length; i ++) {
                    msg = msg.replace ('{}', format (arguments[i]));
                }
                let line = $('<div/>').text (msg).appendTo (root);
                if (replace) {
                    line.css ('color', colors[index ++ % 2]);
                }
                return this;
            },
            clear : function () {
                root.empty ();
                return this;
            },
            replaceColor: value => {
                replace = value;
                if (!replace) {
                    index = 0;
                }
            }
        };
    })();

    function fetch_values () {
        controls.each ((i, c) => {
            let id = $(c).attr ('id').split ('-')[1];
            p[id] = parseFloat ($(c).val ());
        });
    }

    function format (n) {
        return Math.round (n * 1000) / 1000;
    }

    function set_size () {
        let canvas = $('#canvas'), pos = canvas.offset (), H = $(window).height (), h = H - pos.top;
        canvas.css ('height', h);
        $('#log-panel').css ('height', h);
    }

    function draw () {
        logger.clear ().replaceColor (false);
        chart.clear ();
        fetch_values ();
        logger.info (p).replaceColor (true);

        data = [[], []];
        s1 = {error: 0, sum: 0, last_error: 0, value: 280, matches: 0, name:"left"};
        s2 = {error: 0, sum: 0, last_error: 0, value: 390, matches: 0, name:"right"};
        count = 0;
        /*error = sum = last_error = */
        // count = matches = 0;

        // data.push ({round: 0, offset: format (p.target - p.value), sum: 0, inc: 0, value: format (p.value)});

/*
        while (!pid ()) {
            logger.info (data [count - 1]);
            p.value = sampling (p.value);
        }
*/
        data[0].push ({round: 0, offset: 0, sum: 0, inc: 0, value: format (s1.value)});
        data[1].push ({round: 0, offset: 0, sum: 0, inc: 0, value: format (s2.value)});
        while (true) {
            let n1 = pid (s1);
            // logger.info (data [count - 1]);
            s1.value = sampling (s1.value, n1);
            data[0].push ({round: count, offset: format (s1.error), sum: format (s1.sum), inc: format (n1), value: format (s1.value)});
            logger.info ('[ left] error = {}, sum = {}, inc = {}, value = {}', s1.error, s1.sum, n1, s1.value);

            let n2 = pid (s2);
            s2.value = sampling (s2.value, n2);
            data[1].push ({round: count, offset: format (s2.error), sum: format (s2.sum), inc: format (n2), value: format (s2.value)});
            logger.info ('[right] error = {}, sum = {}, inc = {}, value = {}', s2.error, s2.sum, n2, s2.value);
            if ((s1.matches >= 5 && s2.matches >= 5) || count > 1000) {
            // if (matches >= 5 || count > 1000) {
                break;
            }
            count ++;
        }

        options.xAxis.data = [];
        let d1 = [], d2 = [], d3 = [];
        for (let i = 0; i < data[0].length; i ++) {
            options.xAxis.data.push (i);
            d1.push (p.target);
            d2.push (data[0][i].value);
            d3.push (data[1][i].value);
        }
/*
        data.forEach (e => {
            options.xAxis.data.push (e.round);
            d1.push (p.target);
            d2.push (e.value);
        });
*/
        options.series = [];
        options.series.push ({
            data: d1, type: 'line', smooth: false, name: 'base', showSymbol: false
        });
        options.series.push ({
            data: d2, type: 'line', smooth: true, name: 'value-left'
        });
        options.series.push ({
            data: d3, type: 'line', smooth: true, name: 'value-right'
        });
        chart.setOption (options);
    }

    function sampling (value, d) {
        let n = Math.random() - .5, x = Math.random () * .1;
        return value + d + d * n/* * x*/;  // +- 5% 扰动
    }

    $(window).on ('resize', () => {
        set_size ();
        chart.resize ();
    });

    $('#btn-apply').on ('click', draw);
    controls.on ('change', draw);

    set_size ();
    chart = echarts.init (document.getElementById ('canvas'));

    draw ();
});
</script>
</body>
</html>